﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace PhiStore.Threading
{
    /// <summary>
    /// This is a thread safe channel (queue) for many-to-many relations.
    /// It utilizes poisons as a way to terminate threads using the channel.
    /// </summary>
    /// <typeparam name="T">The type of object for the channel</typeparam>
    public class Channel<T>
    {
        private Queue<T> queue; //internal queue
        private int capacity;
        private bool poisoned;

        //Monitor object!
        private object monitor = new object();

        /// <summary>
        /// Unbounded (internal queue is bounded by address space and/or available memory)
        /// </summary>
        public Channel() : this(int.MaxValue) { }

        /// <summary>
        /// Constructor for Channel
        /// </summary>
        /// <param name="capacity">Number of elements in queue</param>
        public Channel(int capacity)
        {
            this.queue = new Queue<T>();
            this.capacity = capacity;
            this.poisoned = false;
        }

        /// <summary>
        /// Returns the number of elements in the queue
        /// </summary>
        public int Count { get { lock (monitor) { return queue.Count; } } }

        private void pushToQueue(T obj)
        {
            if (poisoned) //aarrrghhhh
            {
                if (queue.Count == 0) //someone may be waiting
                {
                    Monitor.PulseAll(monitor);
                }
                throw new PoisonException();
            }

            queue.Enqueue(obj);

            if (queue.Count == 1)
            {
                Monitor.PulseAll(monitor);
            }
        }

        /// <summary>
        /// Adds an element to the queue. Throws PoisonException if channel is poisoned.
        /// </summary>
        /// <param name="obj">The element to add</param>
        public void Send(T obj)
        {
            lock (monitor)
            {
                while (queue.Count == capacity && !poisoned)
                {
                    Monitor.Wait(monitor);
                }

                pushToQueue(obj);
            }
        }


        /// <summary>
        /// Adds an element to the queue iff there is available capacity and channel isn't poisoned. 
        /// Does not throw PoisonException.
        /// </summary>
        /// <param name="obj">The element to add</param>
        /// <returns>success/failure</returns>
        public bool TrySend(T obj)
        {
            bool result = true;
            lock (monitor)
            {
                if (queue.Count == capacity || poisoned)
                {
                    result = false;
                }
                else
                {
                    pushToQueue(obj);
                }
            }
            return result;
        }


        private T removeFromQueue()
        {
            if (queue.Count == 0 && poisoned) //aaarrrgggh
            {
                Monitor.PulseAll(monitor);
                throw new PoisonException();
            }

            T item = queue.Dequeue();

            if (queue.Count == (capacity - 1))
            {
                Monitor.PulseAll(monitor);
            }
            return item;
        }

        /// <summary>
        /// Returns the element first in the queue. Throws PoisonException if queue is empty and channel is poisoned.
        /// </summary>
        /// <returns>An element</returns>
        public T Receive()
        {
            lock (monitor)
            {
                T item;
                while (queue.Count == 0 && !poisoned)
                {
                    Monitor.Wait(monitor);
                }
                item = removeFromQueue();
                return item;
            }
        }

        /// <summary>
        /// Returns the element first in the queue. Throws PoisonException if queue is empty and channel is poisoned.
        /// Throws TimeOutException if queue is empty and no item has been added within specified timeout
        /// </summary>
        /// <param name="millisecondsTimeout">Number of milliseconds before timeout exception</param>
        /// <returns>An element</returns>
        public T Receive(int millisecondsTimeout)
        {
            lock (monitor)
            {
                T item;
                if (queue.Count == 0 && !poisoned)
                {
                    Monitor.Wait(monitor, millisecondsTimeout);
                }
                if (queue.Count > 0 || poisoned)
                    item = removeFromQueue();
                else
                    throw new TimeoutException("ReceiveTimeout");

                return item;
            }
        }

        /// <summary>
        /// Returns the element first in the queue iff the queue isn't empty.
        /// Does not throw PoisonException.
        /// </summary>
        /// <param name="item">The element received</param>
        /// <returns>success/failure</returns>
        public bool TryReceive(out T item)
        {
            bool result = true;
            item = default(T);
            lock (monitor)
            {
                if (queue.Count == 0)
                {
                    result = false;
                }
                else
                {
                    item = removeFromQueue();
                }
            }
            return result;
        }

        /// <summary>
        /// Returns true
        /// </summary>
        public bool IsSynchronized
        {
            get { return true; }
        }

        /// <summary>
        /// Returns the capacity of the queue. int.MaxValue is unbounded.
        /// </summary>
        public int Capacity
        {
            get { return capacity; }
        }

        /// <summary>
        /// Poisons the channel. No more elements can be added to the queue.
        /// </summary>
        public void Poison()
        {
            lock (monitor) 
            { 
                this.poisoned = true;
                if (queue.Count == 0 || queue.Count == capacity)
                {
                    Monitor.PulseAll(monitor);
                }
            }
        }

        /// <summary>
        /// Returns whether or not the channel is poisoned.
        /// </summary>
        public bool IsPoisoned
        {
            get { lock (monitor) { return this.poisoned; } }
        }
    }

    /// <summary>
    /// Class for specific exception related to channels.
    /// </summary>
    public class PoisonException : Exception
    {
        /// <summary>
        /// Constructor
        /// </summary>
        public PoisonException()
            : base("Poisoned like crazy!")
        {
        }
    }
}
